using System;
using System.Collections;
using Sedna.Core.Persistence;

namespace Sedna.Domain
{
	/// <summary>
	/// read-only value object to represent monetary amounts in domain model
	/// Apart from the amount in its smallest part (e.g. USD1/Factor) it contains 
	/// a currency code. Although so far the System does not support multy-currency
	/// operations, in future it can deal with amounts in different currencies. 
	/// Now all amounts are implicitely converted to USD and stored in the underlying
	/// DB (by means of <see cref="MonetaryAmountUsdType"/>)
	/// </summary>
	[Serializable]
	public class MonetaryAmount
    {
        #region Fields

        internal static readonly string NUL = "NUL";//this designates a NULL value in the Database (where applicable)
		public static readonly string USD = "USD";
		public static readonly string RUR = "RUR";
		public static readonly string EUR = "EUR";
		public static readonly string GBP = "GBP";

        private static readonly IDictionary zeroAmount;
	    
		public static readonly MonetaryAmount Default = new MonetaryAmount(NUL, 0);
//		public static readonly MonetaryAmount ZeroUSD = new MonetaryAmount(USD, 0);

		/// <summary>
		/// this value is used to convert from amount in full units (e.g. in dollars) into 
		/// fractional parts. (e.g. 10 dollars is 10*10 000 = 100 000 units)
		/// </summary>
		public static readonly int Factor = 10000;

		private string currency;

		//amount in finest units (e.g. USD 10E-4 or in other words the smallest unit is USD 0.0001)
		private long amount;

        #endregion

        #region Constructors
	    
        static MonetaryAmount()
	    {
            zeroAmount = new Hashtable();
            zeroAmount[NUL] = new MonetaryAmount(NUL, 0);
            zeroAmount[USD] = new MonetaryAmount(USD, 0);
            zeroAmount[RUR] = new MonetaryAmount(RUR, 0);
            zeroAmount[EUR] = new MonetaryAmount(EUR, 0);
            zeroAmount[GBP] = new MonetaryAmount(GBP, 0);
        }

        public MonetaryAmount()
		{
		}

		private MonetaryAmount(string currency, long amount)
		{
			this.currency = currency;
			this.amount = amount;
        }

        #endregion

        #region Static methods

        public static MonetaryAmount GetZeroAmount(string currency)
	    {
            return (MonetaryAmount) zeroAmount[currency];
	    }
	    
		/// <summary>
		/// creates a MonetaryAmount in USD from the specified value in factored units (e.g. USD1E-4)
		/// </summary>
		/// <param name="amount"></param>
		/// <returns></returns>
		public static MonetaryAmount CreateFromFactoredAmount(long amount)
		{
			return CreateFromFactoredAmount(USD, amount);
		}

		/// <summary>
		/// creates a MonetaryAmount in specified currency from the specified value in factored units (e.g. USD1E-4)
		/// </summary>
		/// <param name="currency">currency code - note only those listed above currency codes are supported</param>
		/// <param name="amount">amount in factored units</param>
		/// <returns>initialized instance of a MonetaryAmount</returns>
		public static MonetaryAmount CreateFromFactoredAmount(string currency, long amount)
		{
			return new MonetaryAmount(currency, amount);
		}
	    
	    public static MonetaryAmount CreateFromValue(decimal  value)
	    {
	        long amount = (long)(value*Factor);
            return CreateFromFactoredAmount(amount);
	    }
	    
	    public static MonetaryAmount CreateFromValue(string currency, decimal  value)
	    {
	        long amount = (long)(value*Factor);
            return CreateFromFactoredAmount(currency, amount);
	    }

		/// <summary>
		/// copy constructor. this constructor is of little use since the object is immutable
		/// </summary>
		/// <param name="monetaryAmount"></param>
		/// <returns></returns>
		public static MonetaryAmount Create(MonetaryAmount monetaryAmount)
		{
			return new MonetaryAmount(monetaryAmount.Currency, monetaryAmount.Amount);
        }

        #endregion Static methods

        #region Static properties
	    
	    
        #endregion

        #region Properties

        public string Currency
		{
			get { return currency; }
		}
	    
		/// <summary>
		/// amount in factored units
		/// </summary>
		public long Amount
		{
			get { return amount; }
		}
	    
	    
	    public bool IsZero
	    {
            get { return amount.Equals(0); }
	    }
	    
	    public bool HasValue
	    {
            get { return !this.Equals(Default); }
        }

        /// <summary>
        /// returns Amount / Factor
        /// this property must NOT be used in ANY calculations and represents just a convenience method of converting
        /// factored amount into normal usual amount in e.g. USD
        /// </summary>
        public double FullAmount
        {
            get { return (double)amount / Factor; }
        }
	    
        public decimal Value
        {
            get { return (decimal)Amount / Factor; }
        }
	    
        #endregion

        #region Math methods

        public MonetaryAmount Negate()
        {
            return new MonetaryAmount(this.currency, -this.amount);
        }

        public MonetaryAmount Abs()
        {
            return amount >= 0 ? this : new MonetaryAmount(currency, Math.Abs(amount));
        }

        public MonetaryAmount Add(MonetaryAmount that)
        {
            if (that == null || that.IsZero)
            {
                return this;
            }
//            CheckCurrency(this, that);
            return new MonetaryAmount(GetResultCurrency(this, that), this.Amount + that.Amount);
        }



        public MonetaryAmount Add(params MonetaryAmount[] amountsToAdd)
        {
            if (amountsToAdd == null || amountsToAdd.Length == 0)
            {
                return this;
            }
            long result = this.amount;
            string currency = this.Currency;
            foreach (MonetaryAmount amountToAdd in amountsToAdd)
            {
                if (amountToAdd == null)
                {
                    continue;
                }
                currency = GetResultCurrency(currency, amountToAdd.currency);
//                CheckCurrency(this, amountToAdd);
                result += amountToAdd.amount;
            }

            return result != this.amount ? new MonetaryAmount(currency, result) : this;
        }
	    
        public MonetaryAmount Sub(params MonetaryAmount[] amountsToSub)
        {
            if (amountsToSub == null || amountsToSub.Length == 0)
            {
                return this;
            }
            long result = this.amount;
            string currency = this.Currency;
            foreach (MonetaryAmount amountToSub in amountsToSub)
            {
                if (amountToSub == null)
                {
                    continue;
                }
                currency = GetResultCurrency(currency, amountToSub.currency);
//                CheckCurrency(this, amountToAdd);
                result -= amountToSub.amount;
            }

            return result != this.amount ? new MonetaryAmount(currency, result) : this;
        }

        public MonetaryAmount Sub(MonetaryAmount that)
        {
            if (that == null || that.IsZero)
            {
                return this;
            }
//            CheckCurrency(this, that);
            //			if(that == null || that.Currency == NUL || that.IsZero)//NULL - x = x
            //			{
            //				return this;
            //			}
            //			if(that.Currency != this.Currency)
            //			{
            //				throw new ArgumentException(String.Format("Unable to perform math operations on different currencies. Use conversion first. this.Currency={0} that.Currency={1}", this.Currency, that.Currency));
            //			}
            return new MonetaryAmount(GetResultCurrency(this, that), this.Amount - that.Amount);
        }

        public MonetaryAmount Mul(long scalar)
        {
            return new MonetaryAmount(this.Currency, this.Amount * scalar);
        }

        public MonetaryAmount[] Distribute(int number)
        {
            long valuePer = this.Amount / number;
            long rest = this.Amount % number;
            MonetaryAmount[] values = new MonetaryAmount[number];
            for (int i = 0; i < number; i++)
            {
                if (rest == 0)
                {
                    values[i] = MonetaryAmount.CreateFromFactoredAmount(this.Currency, valuePer);
                }
                else
                {
                    values[i] = MonetaryAmount.CreateFromFactoredAmount(this.Currency, valuePer + 1);
                    rest--;
                }
            }

            return values;
        }

        public static MonetaryAmount Sum(params MonetaryAmount[] values)
        {
            if (values == null || values.Length == 0)
            {
                return MonetaryAmount.Default;
            }
            string currency = MonetaryAmount.NUL;
            long amount = 0;
            for (int i = 0; i < values.Length; i++)
            {
                if (values[i] == null || values[i].IsZero)
                {
                    continue;
                }
                currency = GetResultCurrency(currency, values[i].Currency);
                amount += values[i].Amount;
            }

            return currency == NUL ? MonetaryAmount.Default : MonetaryAmount.CreateFromFactoredAmount(currency, amount);
        }

        public static MonetaryAmount Sum(ICollection col)
        {
            if (col == null)
            {
                return MonetaryAmount.Default;
            }
            MonetaryAmount[] values = new MonetaryAmount[col.Count];
            col.CopyTo(values, 0);
            return Sum(values);
        }

        #endregion Math methods
	    
        #region Public methods
        /// <summary>
	    /// This method should only be used to convert NUL currency values returned by DAO to target currency
	    /// </summary>
	    /// <param name="curr">Target currency</param>
	    /// <returns></returns>
	    public MonetaryAmount DefaultToZero(string curr)
	    {
	        if (this.currency.Equals(curr))
	        {
	            return this;
	        }
            if (HasValue) throw new InvalidOperationException("The method is applicable only to MonetaryAmount.Default & non-NUL currency values");
            return GetZeroAmount(curr);
	    }
	    	    
		public override bool Equals(object obj)
		{
			if(obj==this)
				return true;

			MonetaryAmount that = obj as MonetaryAmount;
			if(obj == null)
				return false;

			return this.amount==that.amount && this.currency == that.currency;
		}

		public override int GetHashCode()
		{
			return (int)amount;
		}

		public override string ToString()
		{
			return this.currency+this.Value;
		}

        #endregion Public methods

        #region Private methods

        private static void CheckCurrency(MonetaryAmount a, MonetaryAmount b)
        {
            if (a == null || b == null)
            {
                return;
            }
            CheckCurrency(a.currency, b.currency);
        }

	    private static void CheckCurrency(string a, string b)
	    {
	        if (a == NUL || b == NUL)
	        {
                return;
//	            throw new ArgumentException("Unable to perform math operations on NUL currency.");
	        }
	        if (a != b)
	        {
	            throw new ArgumentException(String.Format("Unable to perform math operations on different currencies. Use conversion first. a.Currency={0} b.Currency={1}", a, b));
	        }
        }

	    private static string GetResultCurrency(MonetaryAmount a, MonetaryAmount b)
	    {
            return GetResultCurrency(a.Currency, b.Currency);
	    }
	    
	    private static string GetResultCurrency(string a, string b)
	    {
	        if (a.Equals(NUL))
	        {
                return b;
	        }
	        if (b.Equals(NUL))
	        {
                return a;
	        }
	        if (a.Equals(b))
	        {
                return a;
	        }
            throw new ArgumentException(String.Format("Unable to perform math operations on different currencies. Use conversion first. a.Currency={0} b.Currency={1}", a, b));
        }
	    
        #endregion
    }
}
